"""
Dependencies:
pyPortMidi

Works with TouchOSC from Hexler
"""

import pypm
import OSC

debug = True

pypm.Initialize() # always call this first, or OS may crash when you try to open a stream
import socket 
#s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 
#routerIPaddress = '192.168.3.1'
#s.connect((routerIPaddress, 0)) 
IPaddress = '169.254.143.198' #s.getsockname()[0] #own ip address
remoteIPaddress = '169.254.228.161'
print "Local IP address is: " + IPaddress
incomingPort = 8000
outgoingPort = 9000
import os
outputMidiPortName = 'MIDI Yoke NT:  2'
if os.uname()[0] == 'Darwin':
	outputMidiPortName = 'IAC Driver Bus 1'

midiPortName = ""
midiPortNum = 0
while 1:
    sysName, midiPortName, isInput, isOutput, a = pypm.GetDeviceInfo(midiPortNum)
    if ((midiPortName == outputMidiPortName) & isOutput):
        break
    midiPortNum += 1

latency = 0
midiOutput = pypm.Output(midiPortNum, latency)

def myTest():
    OSC.init()
    
##    OSC.createListener() # this defaults to port 9001 as well
    OSC.listen(IPaddress, incomingPort)

    # bind addresses to functions -> oscMessage() function will be triggered everytime a
    # message arrives with a label that has not been previously bound
    OSC.bind(oscMessage, "default")

    import time # in this example we will have a small delay in the while loop

    print 'ready to receive and send osc messages ...'
    
    try:
        while 1:
    ##        OSC.sendMsg("/test", [444], IPaddress, 9000) # send normal msg to a specific ip and port
            OSC.sendMsg("/1/fader1", [0.5], remoteIPaddress, outgoingPort) # !! it sends by default to localhost ip "127.0.0.1" and port 9000 
            # create and send a bundle
            bundle = OSC.createBundle()
            OSC.appendToBundle(bundle, "/test/bndlprt1", [1, 2, 3]) # 1st message appent to bundle
            OSC.appendToBundle(bundle, "/test/bndlprt2", [4, 5, 6]) # 2nd message appent to bundle
    ##        OSC.sendBundle(bundle, IPaddress, 9000) # send it to a specific ip and port

            OSC.sendBundle(bundle) # !! it sends by default to localhost ip "127.0.0.1" and port 9000 
            #OSC.getOSC(inSocket) # listen to incomming OSC in this socket
            time.sleep(0.5) # you don't need this, but otherwise we're sending as fast as possible.
    except KeyboardInterrupt:
        OSC.dontListen() # finally close the connection bfore exiting or program
        raise SystemExit
    OSC.dontListen() # finally close the connection bfore exiting or program


""" Below are some functions dealing with OSC messages RECEIVED to Python.

    Here you can set all the responders you need to deal with the incoming
    OSC messages. You need them to the callBackManager instance in the main
    loop and associate them to the desired OSC addreses like this for example
    addressManager.add(printStuff, "/print")
    it would associate the /print tagged messages with the printStuff() function
    defined in this module. You can have several callback functions in a separated module if you wish
"""

def oscMessage(*msg):
    mes, x = msg
    addr = mes[0]
    numVals = len(mes[1])
    vals = mes[2:]
    map(addr, vals)
    #for i in vals:
    #    type, channel, CC = mapping(addr)
    #    value = int(vals[0] * 127)
    #    if (type != 'None'):
    #        midiMessage(type, channel, CC, value)
    
    
def map(key, vals):
    print key, vals
    tags = key.split('/')
    page = tags[1]
    if page == 'ping':
        #OSC.sendMsg("/ping", [0.5], '192.168.3.3',9000) # !! it sends by default to localhost ip "127.0.0.1" and port 9000 
        print key
    elif page == 'accxyz':
        acceration(vals)
    else:
        assert(page.isdigit())
        controlKind = tags[2].rstrip('1234567890')
        controlNum = tags[2].strip('abcdefghijklmnopqrstuvwxyz')
        for v in vals:
        	if v == 'z':
        		return None
        if key.find('z') != -1:
        	return None
        if controlNum == '':
            controlNum = '0'
        if (len(tags) > 3):
            controlSubNums = tags[3:]
            if (len(controlSubNums) == 1):
                multiFader(page, controlKind, controlNum, controlSubNums, vals)
            elif (len(controlSubNums) == 2):
                multiToggle(page, controlKind, controlNum, controlSubNums, vals)
        else:
            if (len(vals) == 1):
                singleValuedControl(page, controlKind, controlNum, vals)
            else:
                xyControl(page, controlKind, controlNum, vals)
       

def singleValuedControl(page, controlKind, controlNum, vals):
    assert(len(vals) == 1)
    type, channel, CC = mapTable(controlKind)
    channel = int(page)
    CC += (int(controlNum) - 1)
    value = int(vals[0] * 127)
    midiMessage(type, channel, CC, value)

def multiFader(page, controlKind, controlNum, controlSubNums, vals):
    assert(len(vals) == 1)
    assert(len(controlSubNums) == 1)
    type, channel, CC = mapTable(controlKind)
    channel = int(page)
    CC += (int(controlNum) - 1) * 24 + (int(controlSubNums[0]) - 1)
    value = int(vals[0] * 127)
    midiMessage(type, channel, CC, value)
    
def multiToggle(page, controlKind, controlNum, controlSubNums, vals):
    assert(len(vals) == 1)
    assert(len(controlSubNums) == 2)
    type, channel, CC = mapTable(controlKind)
    channel = int(page)
    print controlSubNums[0], controlSubNums[1]
    CC += (int(controlSubNums[0]) - 1) * 8 + (int(controlSubNums[1]) - 1)
    value = int(vals[0] * 127)
    midiMessage(type, channel, CC, value)

#The xyCycle feature makes it easier to use a "midi learn" function in the DAW
xyCycle = 0
def xyControl(page, controlKind, controlNum, vals):
    global xyCycle
    assert(len(vals) == 2)
    assert(controlKind == 'xy')
    type, channel, CC = mapTable(controlKind)
    channel = int(page)
    CC += (int(controlNum)-1) * 2
    if xyCycle == 0:
        value = int(vals[0] * 127)
        midiMessage(type, channel, CC, value)
        value = int(vals[1] * 127)
        midiMessage(type, channel, CC + 1, value)
        xyCycle = 1
    else:
        value = int(vals[1] * 127)
        midiMessage(type, channel, CC + 1, value)
        value = int(vals[0] * 127)
        midiMessage(type, channel, CC, value)
        xyCycle = 0
        
def mapTable(key):
    map = { \
    #(Kind, Channel, CC)
    #accell uses this CC value and the above it.
    'accel': ('CC',16,100), \
    'fader': ('CC',1,1), \
    'toggle': ('CC',1,10), \
    'push': ('NoteOn',1,40), \
    'xy': ('CC',1,20), \
    'multitoggle': ('NoteOn',1,23), \
    'multifader': ('CC',1,50), \
    'rotary': ('CC',1,30), \
    'default': (1,0) \
    }
    result = map[key]
    return result
    
"""
possible controls are:
fader
toggle
push
xy
multitoggle
rotary
multifader
led (display only)
"""
        
# def 
        
# def controlType():
    # funcMap = {
    # "a": a,
    # "A": a,
    # "b": b,
    # "exoticName": c }

    # funcString = GetFunctionAsString()
    # try:
        # f = funcMap(funcString)
    # except KeyError:
        # print "No such function"
        # raise SomethingElse
    # result = f(args)
        

        
def acceration(vals):
    for i in range(len(vals)):
        type, channel, CC = mapTable('accel')
        CC += i
        accRange = 5
        value = int(((vals[i] + (accRange/2.0)) / accRange) * 127)
        if value > 127:
            value = 127
            if debug: 
                print "ACC Over", i, vals[i], " -> ", value
        if value < 0:
            value = 0
            if debug: 
                print "ACC Undr", i, vals[i], " -> ", value
        midiMessage(type, channel, CC, value)


def oldMapTable(key):
    #TouchOSC 1.2
    map = { \
    #Simple - pg 1
    #(Channel, CC)
    'fader1': ('CC', 1,1), \
    'fader2': ('CC', 1,2), \
    'fader3': ('CC', 1,3), \
    'fader4': ('CC', 1,4), \
    'fader5': ('CC', 1,5), \
    'toggle1': ('CC', 1,6), \
    'toggle2': ('CC', 1,7), \
    'toggle3': ('CC', 1,8), \
    'toggle4': ('CC', 1,9), \
    #Simple - pg 2
    'push1': ('CC', 1,10), \
    'push2': ('CC', 1,11), \
    'push3': ('CC', 1,12), \
    'push4': ('CC', 1,13), \
    'push5': ('CC', 1,14), \
    'push6': ('CC', 1,15), \
    'push7': ('CC', 1,16), \
    'push8': ('CC', 1,17), \
    'push9': ('CC', 1,18), \
    'push10': ('CC', 1,19), \
    'push11': ('CC', 1,21), \
    'push12': ('CC', 1,22), \
    'push13': ('CC', 1,23), \
    'push14': ('CC', 1,24), \
    'push15': ('CC', 1,25), \
    'push16': ('CC', 1,26), \
    'toggle1': ('CC', 1,27), \
    'toggle2': ('CC', 1,28), \
    'toggle3': ('CC', 1,29), \
    'toggle4': ('CC', 1,30), \
    #Simple - pg 3
    'toggle1': ('CC', 1,31), \
    'toggle2': ('CC', 1,32), \
    'toggle3': ('CC', 1,33), \
    'toggle4': ('CC', 1,34), \
    'xy': (['CC', 'CC'], [1,1],[35, 36]), \
    #Simple - pg 4
    'toggle1': ('CC', 1,37), \
    'toggle2': ('CC', 1,38), \
    'toggle3': ('CC', 1,39), \
    'toggle4': ('CC', 1,40), \
    #'/4/multitoggle': (1,41), \
    'default': (1,0) \
    }
    result = ()
    try:
        result = map[key]
    except KeyError:
        if (key.find('/4/multitoggle') != -1):
            #column = key[15]
            #row = key[17]
            #import pdb; pdb.set_trace()
            result = ('CC', 1, (int(key[15])*8+int(key[17])))
        else:
            result = ('None', 0, 0)
    #print result
    #import pdb; pdb.set_trace()
    return result

def midiMessage(type, channel, d1, d2):
    typelist = {'NoteOff': 0x80, 'NoteOn': 0x90, 'PolyAft': 0xA0, \
    'CC': 0xB0, 'Program': 0xC0, 'ChAft': 0xD0, 'Wheel': 0xE0}
    assert(channel < 17)
    assert(channel > 0)
    assert(d1 > -1)
    assert(d1 < 128)
    assert(d2 > -1)
    assert(d2 < 128)
    if debug:
        print type, channel, d1, d2
    try:
        msg = [typelist[type],d1,d2]
    except KeyError:
        print "Bad MIDI message type!"
        print type, "is not a valid type!"
        assert(0)
    msg[0] += channel - 1
    midiOutput.Write([[msg,pypm.Time()]])
    
"""
Message	                Status	    Data 1	        Data 2
Note Off	            8n	        Note Number	    Velocity
Note On	                9n	        Note Number	    Velocity
Polyphonic Aftertouch	An	        Note Number	    Pressure
Control Change	        Bn	        Controller Number	Data
Program Change	        Cn	        Program Number	Unused
Channel Aftertouch	    Dn	        Pressure	    Unused
Pitch Wheel	            En	        LSB 	        MSB
"""
if __name__ == '__main__': myTest()